<?php

declare(strict_types=1);

namespace App\Command\Migrate;

use App\Migration\MigrationInterface;
use PDO;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use Yiisoft\Aliases\Aliases;
use Yiisoft\Yii\Console\ExitCode;

#[AsCommand(
    name: 'migrate/up',
    description: 'Apply all pending database migrations.',
)]
final class Up extends Command
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly Aliases $aliases,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $directory = $this->aliases->get('@root/migrations');

        if (!is_dir($directory)) {
            $output->writeln('<info>No migrations directory found. Nothing to do.</info>');

            return ExitCode::OK;
        }

        $migrations = $this->loadMigrations($directory);
        if ($migrations === []) {
            $output->writeln('<info>No migrations to execute.</info>');

            return ExitCode::OK;
        }

        $this->ensureMigrationTable();
        $applied = $this->getAppliedMigrations();

        foreach ($migrations as $migration) {
            $id = $migration->getId();
            if (isset($applied[$id])) {
                $output->writeln(sprintf('<comment>Skipping %s — already applied.</comment>', $id));
                continue;
            }

            $output->write(sprintf('Applying %s ... ', $id));

            $this->pdo->beginTransaction();
            try {
                $migration->up($this->pdo);
                $this->recordMigration($id);
                $this->pdo->commit();
            } catch (Throwable $exception) {
                $this->pdo->rollBack();

                throw new RuntimeException(
                    sprintf('Failed to apply migration %s: %s', $id, $exception->getMessage()),
                    previous: $exception,
                );
            }

            $output->writeln('<info>done.</info>');
        }

        return ExitCode::OK;
    }

    /**
     * @return list<MigrationInterface>
     */
    private function loadMigrations(string $directory): array
    {
        $files = glob($directory . DIRECTORY_SEPARATOR . '*.php');
        if ($files === false) {
            return [];
        }

        sort($files);

        $migrations = [];
        foreach ($files as $file) {
            $migration = require $file;
            if (!$migration instanceof MigrationInterface) {
                throw new RuntimeException(sprintf('Migration file %s must return an instance of %s.', $file, MigrationInterface::class));
            }

            $migrations[] = $migration;
        }

        return $migrations;
    }

    private function ensureMigrationTable(): void
    {
        $this->pdo->exec(
            <<<'SQL'
            CREATE TABLE IF NOT EXISTS app_migrations (
                id VARCHAR(180) PRIMARY KEY,
                run_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
            )
            SQL,
        );
    }

    /**
     * @return array<string, true>
     */
    private function getAppliedMigrations(): array
    {
        $statement = $this->pdo->query('SELECT id FROM app_migrations');
        $rows = $statement === false ? [] : $statement->fetchAll(PDO::FETCH_COLUMN);

        $applied = [];
        foreach ($rows as $id) {
            if (!is_string($id)) {
                continue;
            }
            $applied[$id] = true;
        }

        return $applied;
    }

    private function recordMigration(string $id): void
    {
        $statement = $this->pdo->prepare('INSERT INTO app_migrations (id) VALUES (:id)');
        $statement->bindValue(':id', $id, PDO::PARAM_STR);
        $statement->execute();
    }
}
